03_Punto-de-Partida/03 -- Chatbot.md

Durante este tercer subcapítulo atacaremos directamente al actor principal del proyecto, el chatbot. Primero de todo analizaremos cómo está desarrollado el chatbot en sí, el código Python.

Pese a ser de increíble importancia, tampoco se profundizará en el código de forma extensa ya que el agente Python será descartado para el propio TFG. Por ello mismo se analizará más bien la arquitectura, flujo y patrones del agente en sí.

El agente se compone de cinco capas diferenciadas:

  • Flask (app.py): servidor HTTP de entrada que recibe las peticiones del frontend y las delega al Runner de Google ADK.
  • Runner (Google ADK): gestiona sesiones y memoria (InMemorySessionService, InMemoryMemoryService) y hace de intermediario entre Flask y el agente.
  • LlmAgent (agent.py): el agente en sí, instanciado con Gemini 2.5 Flash. No ejecuta herramientas directamente — le pide al MCP Server que lo haga.
  • MCP Server (mcp_server.py): proceso independiente que expone las herramientas via protocolo MCP sobre HTTP en el puerto 8081.
  • tool_functions.py: implementaciones reales de cada herramienta (llamadas a la API de DWall, generación de SQL, búsqueda semántica, etc.).

El patrón MCP

MCP (Model Context Protocol) es un protocolo estándar que permite a los LLMs acceder a herramientas externas de forma estructurada. En lugar de programar el flujo de decisión en código, el LLM recibe un catálogo de herramientas y decide autónomamente cuáles invocar, en qué orden y con qué parámetros, en función de la pregunta del usuario.

El servidor MCP se implementa con la librería FastMCP. Cada herramienta se declara con el decorador @mcp.tool() y se expone sobre transporte HTTP:

from fastmcp import FastMCP
mcp = FastMCP("sql-chatbot-mcp-server")

@mcp.tool()
def get_variable_id(question: str) -> int:
    """Finds the variable ID for a given natural language question."""
    return tool_functions.get_variable_id(question)

@mcp.tool()
def execute_sql(queries_list: List[Dict], timeseries: bool, user_question: str):
    """Executes a SQL query."""
    return tool_functions.execute_sql(queries_list, timeseries, user_question)

# ...
mcp.run(transport='http', host="0.0.0.0", port=8081)

Al recibir una pregunta, Gemini evalúa el catálogo de herramientas disponibles y decide el plan de ejecución. Por ejemplo, para "¿Cuál es el rendimiento medio de Ulia 14?" el agente llamará primero a get_variable_id, luego a timestamp_variable_sql o general_sql_generator, y finalmente a execute_sql. Esta orquestación es completamente autónoma — no hay lógica en código que decida el orden.

RAG en el agente

El agente aplica RAG en tres puntos distintos del flujo:

1. Identificación de variables (get_variable_id) La resolución de nombres se hace con búsqueda híbrida sobre FastText, combinando similitud semántica y coincidencia de palabras clave para identificar la variable más probable dado un nombre en lenguaje natural.

2. Generación de SQL (general_sql_generator) Para generar SQL se inyecta contexto del esquema de base de datos recuperado por similitud semántica desde FAISS. Hay dos stores:

  • vector_db/: contiene resúmenes de tablas y relaciones del esquema de DWall, generados a partir de database_structure.json. Se usa para dar al LLM contexto sobre qué tablas y columnas existen antes de pedir que genere una query.
  • feedback_db/: contiene pares pregunta-SQL-respuesta de interacciones pasadas validadas por el usuario. Se usa como contexto adicional para que el LLM pueda reutilizar queries que ya funcionaron.

3. Búsqueda en documentos (file_search_analysis) Los documentos subidos por los usuarios (PDFs, Word, etc.) se procesan en chunks y se indexan también en FAISS. La herramienta realiza una búsqueda semántica sobre estos chunks para complementar las respuestas de SQL con información documental relevante. El agente está instruido para usarla siempre como complemento, incluso en preguntas de SQL, enriqueciendo cualquier respuesta con contexto documental relevante.